package com.hockeo.tinygds;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.regex.Pattern;

import com.google.appengine.api.datastore.Cursor;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.QueryResultIterable;
import com.google.appengine.api.datastore.QueryResultIterator;
import com.google.appengine.api.datastore.QueryResultList;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Query.FilterPredicate;
import com.google.appengine.api.datastore.Query.SortDirection;
import com.google.appengine.api.datastore.Query.SortPredicate;

/**
 * Allows to query datastore entities. Instead of returning raw entities, this class
 * returns real objects. New TQuery objects can be obtained by calling
 *
 * @param <T> the class for which the datastore should be queried
 * @version $Id$
 * @author jjanke
 */
public class TQuery<T>
{
  private static final String  ID_PROPERTY         = "id";
  private static final String  GAE_KEY_PROPERTY    = "__key__";
  private static final Pattern REGEXP_FILTER_SPLIT = Pattern.compile( "\\s" );

  private final TinyGDSFactory d_factory;
  private final TinyGDS        d_tgds;
  private final TMetaData<T>   d_metadata;
  private final Query          d_query;
  private final boolean        d_fLimitedHierarchy;

  private Integer              d_nLimit;
  private Integer              d_nOffset;
  private Cursor               d_cursor;

  TQuery( TinyGDSFactory factory, TinyGDS parent, Class<T> cls )
  {
    d_factory = factory;
    d_tgds = parent;
    d_metadata = d_factory.getMetaData( cls );
    d_query = new Query( d_metadata.getKind() );
    d_fLimitedHierarchy = doesQueryNeedDiscriminatorFilter( d_query, d_metadata );
  }

/**
   * <p>
   * Adds a filter based on the given condition and comparison value. See the following
   * examples for details:
   * </p>
   *
   * <ul>
   * <li>{@code filter("age >", age)}</li>
   * <li>{@code filter("age <", age)}</li>
   * <li>{@code filter("age >=", age)}</li>
   * <li>{@code filter("age <=", age)}</li>
   * <li>{@code filter("age =", age)}</li>
   * <li>{@code filter("age", age)} (if no operator, = is assumed)</li>
   * <li>{@code filter("age !=", age)}</li>
   * <li>{@code filter("age in", ageList)}</li>
   * </ul>
   *
   * <p>
   * The property names used with this method correspond to the names of the persistent member
   * variables unless a variable has a name specified with @TName. In that case, the name specified
   * with @TName needs to be used.
   * </p>
   *
   * <p>
   * You can filter on @TId properties <strong>if</strong> this query is restricted to a
   * Class<T> and the entity has no @TParent. If you are having trouble working around this
   * limitation, please consult the objectify-appengine google group. If you want to filter by
   * id you <strong>must</string> use the name <code>id</code> to refer to the entities @TId field
   * regardless of the actual variable name.
   * </p>
   * <p>
   * You can <strong>not</strong> filter on @TParent properties. Use the {@code ancestor()}
   * method instead.
   * </p>
   *
   * @param strCondition the name of the property to filter by followed by a space and the
   * chosen operator (if no operator is specified, = (equal) is assumed as operator)
   * @param objValue the value (or the list of values) to be used as filter criteria
   * @return this TQuery instance in order to allow chaining other method calls
   */
  public TQuery<T> filter( String strCondition, Object objValue )
  {
    String[] astrCond = REGEXP_FILTER_SPLIT.split( strCondition );

    if ( astrCond.length < 1 || astrCond.length > 2 )
      throw new IllegalArgumentException( "The input condition must consist of a property and possibly an "
          + "operator separated by white space." );

    String strProp = astrCond[0].trim();
    FilterOperator op = astrCond.length == 1 ? FilterOperator.EQUAL : translateOperator( astrCond[1] );

    // check if we are filtering on the @TId field
    //
    if ( strProp.equals( ID_PROPERTY ) )
    {
      if ( d_metadata.hasParentField() )
        throw new IllegalArgumentException( "It is not allowed to filter on " + ID_PROPERTY
            + " if the class to be queried has a @TParent field. The class '" + d_metadata.getEntityClass().getName()
            + "' has such a field." );

      strProp = GAE_KEY_PROPERTY;
      objValue = d_metadata.createRawKey( objValue );
    }
    else
      objValue = makeFilterable( objValue );

    // add the filter to the low-level query
    //
    d_query.addFilter( strProp, op, objValue );

    // return this to allow filter chaining
    //
    return this;
  }

  /**
   * <p>
   * Specifies an order criteria. By default, if no ordering is specified, results are
   * ordered in ascending key order. See ordering examples below:
   * </p>
   *
   * <ul>
   * <li>{@code order("age")} order by the age property in ascending order</li>
   * <li>{@code order("-age")} order by the age property in descending order</li>
   * </ul>
   *
   * <p>
   * The property names used with this method correspond to the names of the persistent
   * member variables unless a variable has a name specified with @TName. In that case,
   * the name specified with @TName needs to be used.
   * </p>
   *
   * <p>
   * It is not possible to order by the @TParent field.
   * </p>
   *
   * @param strProperty the name of the property to order by (possibly preceded by a "-"
   *          (minus) sign to indicate descending order direction
   * @return this TQuery instance in order to allow chaining other method calls
   */
  public TQuery<T> order( String strProperty )
  {
    strProperty = strProperty.trim();
    SortDirection dir = SortDirection.ASCENDING;

    if ( strProperty.startsWith( "-" ) )
    {
      dir = SortDirection.DESCENDING;
      strProperty = strProperty.substring( 1 ).trim();
    }

    // check if we are ordering by the @TId field
    //
    if ( strProperty.equals( ID_PROPERTY ) )
      strProperty = GAE_KEY_PROPERTY;

    d_query.addSort( strProperty, dir );

    return this;
  }

  /**
   * Restricts result set only to objects which have the given ancestor somewhere in the
   * chain. Doesn't need to be the immediate parent.
   *
   * @param objKeyOrEntity can be a Key, a TKey<T>, or a persistent entity object
   * @return this TQuery instance in order to allow chaining other method calls
   */
  public TQuery<T> ancestor( Object objKeyOrEntity )
  {
    d_query.setAncestor( d_factory.getRawKey( objKeyOrEntity ) );
    return this;
  }

  /**
   * Sets the maximum number of results to retrieve at the next fetch.
   *
   * @param nLimit the limit (must be greater or equal to 0, 0 means no limit)
   * @return this TQuery instance in order to allow chaining other method calls
   */
  public TQuery<T> limit( int nLimit )
  {
    d_nLimit = nLimit;
    return this;
  }

  /**
   * Sets the offset from where to start retrieving objects.
   *
   * @param nOffset the offset (>= 0)
   * @return this TQuery instance in order to allow chaining other method calls
   */
  public TQuery<T> offset( int nOffset )
  {
    d_nOffset = nOffset;
    return this;
  }

  /**
   * Sets the cursor indicating from where to start retrieving results at the next fetch.
   *
   * @param cursor the cursor
   * @return this TQuery instance in order to allow chaining other method calls
   */
  public TQuery<T> cursor( Cursor cursor )
  {
    if ( d_fLimitedHierarchy )
      throw new IllegalStateException( "This query does not support a cursor as it already uses "
          + "a special filter on the discriminator to limit the query results to certain sub classes "
          + "of an inheritance hierarchy." );

    d_cursor = cursor;
    return this;
  }

  @Override
  public boolean equals( Object objOtherQuery )
  {
    return d_query.equals( objOtherQuery );
  }

  @Override
  public int hashCode()
  {
    return d_query.hashCode();
  }

  @Override
  public String toString()
  {
    return d_query.toString();
  }

  /**
   * Fetches the results and returns them in a QueryResultIterable.
   */
  public QueryResultIterable<T> asIterable()
  {
    FetchOptions opts = getFetchOptions();

    if ( opts != null )
      return new ToObjectIterable<T>( prepare().asQueryResultIterable( opts ), false );
    else
      return new ToObjectIterable<T>( prepare().asQueryResultIterable(), false );
  }

  /**
   * Fetches the keys of all the results and returns them in a QueryResultIterable.
   */
  public QueryResultIterable<TKey<T>> asIterableKeysOnly()
  {
    FetchOptions opts = getFetchOptions();

    if ( opts != null )
      return new ToObjectIterable<TKey<T>>( prepareKeysOnly().asQueryResultIterable( opts ), true );
    else
      return new ToObjectIterable<TKey<T>>( prepareKeysOnly().asQueryResultIterable(), true );
  }

  /**
   * Fetches the results according to the current limit/offset/cursor settings of this
   * query and returns them in a list.
   */
  public QueryResultList<T> asList()
  {
    FetchOptions opts = getFetchOptions();

    if ( opts == null )
      throw new IllegalStateException( "No limit/offset/cursor has been set. At least one of them "
          + "must be set in order to return results as list." );

    return new ToObjectList<T>( prepare().asQueryResultList( opts ), false );
  }

  /**
   * Fetches the results (keys only) according to the current limit/offset/cursor settings
   * of this query and returns them in a list.
   */
  public QueryResultList<TKey<T>> asListKeysOnly()
  {
    FetchOptions opts = getFetchOptions();

    if ( opts == null )
      throw new IllegalStateException( "No limit/offset/cursor has been set. At least one of them "
          + "must be set in order to return results as list." );

    return new ToObjectList<TKey<T>>( prepareKeysOnly().asQueryResultList( opts ), true );
  }

  /**
   * Returns the first result from this query. Obeys a possibly set offset. If there is no
   * result, <code>null</code> is returned.
   */
  @SuppressWarnings( "unchecked" )
  public T asSingleResult()
  {
    FetchOptions opts = FetchOptions.Builder.withLimit( 1 );

    if ( d_nOffset > 0 )
      opts.offset( d_nOffset );

    Iterator<Entity> it = prepare().asIterator( opts );
    if ( it.hasNext() )
    {
      Entity ent = it.next();
      TMetaData<T> metadata = (TMetaData<T>) d_factory.getMetaData( ent );

      return metadata.toObject( ent );
    }

    return null;
  }

  /**
   * Returns the key of the first result from this query. Obeys a possibly set offset. If
   * there is no result, <code>null</code> is returned.
   */
  @SuppressWarnings( "unchecked" )
  public TKey<T> asSingleResultKey()
  {
    FetchOptions opts = FetchOptions.Builder.withLimit( 1 );

    if ( d_nOffset > 0 )
      opts.offset( d_nOffset );

    Iterator<Entity> it = prepareKeysOnly().asIterator( opts );
    if ( it.hasNext() )
    {
      Entity ent = it.next();
      TMetaData<T> metadata = (TMetaData<T>) d_factory.getMetaData( ent );

      return d_factory.rawKeyToTKey( ent.getKey(), metadata.getEntityClass() );
    }

    return null;
  }

  /**
   * Counts the number of results that currently match this query. Any possibly set
   * limit/offset or cursor is ignored.
   */
  public int count()
  {
    return prepare().countEntities();
  }

  /**
   * Checks if the new a new raw Query is the not the root of its inheritance hierarchy
   * and therefore needs a filter condition on its discriminator value. If the latter is
   * the case, the added IN filter will cause restrictions on cursors etc.
   *
   * @param query the query to be checked
   * @param metadata the metadata of the class for which the query is constructed
   * @return <code>true</code> if the query has been limited to only return entities
   *         matching a specified set of discriminators, otherwise <code>false</code>
   */
  private boolean doesQueryNeedDiscriminatorFilter( Query query, TMetaData<T> metadata )
  {
    // add filter on discriminator column if the given class is not the top-level class of
    // its hierarchy
    //
    if ( !metadata.isRootOfHierarchy() )
    {
      query.addFilter( TMetaData.DISCRIMINATOR_PROPERTY, Query.FilterOperator.IN,
          metadata.getDiscriminatorsOfSameHierarchy() );

      return true;
    }

    return false;
  }

  /**
   * Creates a new PreparedQuery object for this query respecting its current state.
   */
  private PreparedQuery prepare()
  {
    return d_tgds.getDatastoreService().prepare( d_tgds.getTransaction(), d_query );
  }

  /**
   * Creates a new PreparedQuery object for this query respecting its current state.
   */
  private PreparedQuery prepareKeysOnly()
  {
    // copy raw query object (in order to avoid loosing the original query which we might
    // need for further non-key-only queries)
    //
    Query queryCopy = copyRawQuery( d_query );

    queryCopy.setKeysOnly();

    return d_tgds.getDatastoreService().prepare( d_tgds.getTransaction(), queryCopy );
  }

  /**
   * Creates an exact copy of the provided original query.
   */
  private Query copyRawQuery( Query queryOrig )
  {
    Query queryCopy = new Query( queryOrig.getKind(), queryOrig.getAncestor() );

    for ( FilterPredicate filter : queryOrig.getFilterPredicates() )
      queryCopy.addFilter( filter.getPropertyName(), filter.getOperator(), filter.getValue() );

    for ( SortPredicate sort : queryOrig.getSortPredicates() )
      queryCopy.addSort( sort.getPropertyName(), sort.getDirection() );

    return queryCopy;
  }

  /**
   * Converts the textual operator (">", "<=", etc) into a FilterOperator. Forgiving about
   * the syntax; != and <> are NOT_EQUAL, = and == are EQUAL.
   */
  private FilterOperator translateOperator( String strOp )
  {
    strOp = strOp.trim();

    if ( strOp.equals( "=" ) || strOp.equals( "==" ) )
      return FilterOperator.EQUAL;
    else if ( strOp.equals( ">" ) )
      return FilterOperator.GREATER_THAN;
    else if ( strOp.equals( ">=" ) )
      return FilterOperator.GREATER_THAN_OR_EQUAL;
    else if ( strOp.equals( "<" ) )
      return FilterOperator.LESS_THAN;
    else if ( strOp.equals( "<=" ) )
      return FilterOperator.LESS_THAN_OR_EQUAL;
    else if ( strOp.equals( "!=" ) || strOp.equals( "<>" ) )
      return FilterOperator.NOT_EQUAL;
    else if ( strOp.toLowerCase().equals( "in" ) )
      return FilterOperator.IN;
    else
      throw new IllegalArgumentException( "Unknown operator '" + strOp + "'" );
  }

  /**
   * Converts the given value into datastore compatible value. The value <code>null</code>
   * is returned as is, enums are converted into their String representation, TKey
   * instances into Key instances and other persistent entities into their indentifying
   * Key. All other values are returned as is.
   *
   * @param objVal the value to be made filterable
   * @return the filterable object value
   */
  private Object makeFilterable( Object objVal )
  {
    if ( objVal == null )
      return null;
    else if ( objVal instanceof TKey<?> )
      return d_factory.tKeyToRawKey( (TKey<?>) objVal );
    else if ( objVal.getClass().isEnum() )
      return ( (Enum<?>) objVal ).name();
    else
    {
      // it is either an entity (in which case we need to extract its key) or a plain
      // value
      //
      TMetaData<?> md = d_factory.getMetaData( objVal.getClass() );
      if ( md != null )
        return md.getKey( objVal );

      // it is not a key - just return the value as is
      //
      return objVal;
    }
  }

  /**
   * Returns the FetchOptions for the currently set limit, offset and/or cursor.
   */
  private FetchOptions getFetchOptions()
  {
    if ( d_cursor != null )
    {
      FetchOptions opts = FetchOptions.Builder.withCursor( d_cursor );

      if ( d_nLimit != 0 )
        opts = opts.limit( d_nLimit );

      if ( d_nOffset != 0 )
        opts = opts.offset( d_nOffset );

      return opts;
    }
    else if ( d_nLimit != null && d_nLimit > 0 )
    {
      FetchOptions opts = FetchOptions.Builder.withLimit( d_nLimit );

      if ( d_nOffset != 0 )
        opts = opts.offset( d_nOffset );

      return opts;
    }
    else if ( d_nOffset != null && d_nOffset >= 0 )
    {
      FetchOptions opts = FetchOptions.Builder.withOffset( d_nOffset );

      return opts;
    }
    else
    {
      return null;
    }
  }

  /**
   * Iterable that translates from datastore Entity to types Objects
   */
  class ToObjectIterable<S> implements QueryResultIterable<S>
  {
    private final QueryResultIterable<Entity> d_iterableSrc;
    private final boolean                     d_fKeysOnly;

    public ToObjectIterable( QueryResultIterable<Entity> iterableSrc, boolean fKeysOnly )
    {
      d_iterableSrc = iterableSrc;
      d_fKeysOnly = fKeysOnly;
    }

    @Override
    public QueryResultIterator<S> iterator()
    {
      return new ToObjectIterator<S>( d_iterableSrc.iterator(), d_fKeysOnly );
    }
  }

  /**
   * Iterator that translates from datastore Entity to typed Objects
   */
  class ToObjectIterator<S> implements QueryResultIterator<S>
  {
    private final QueryResultIterator<Entity> d_iterSrc;
    private final boolean                     d_fKeysOnly;

    public ToObjectIterator( QueryResultIterator<Entity> iterSrc, boolean fKeysOnly )
    {
      d_iterSrc = iterSrc;
      d_fKeysOnly = fKeysOnly;
    }

    @Override
    public boolean hasNext()
    {
      return d_iterSrc.hasNext();
    }

    @Override
    @SuppressWarnings( "unchecked" )
    public S next()
    {
      Entity entityNext = d_iterSrc.next();
      TMetaData<S> md = (TMetaData<S>) d_factory.getMetaData( entityNext );

      if ( d_fKeysOnly )
        return (S) d_factory.rawKeyToTKey( entityNext.getKey(), d_metadata.getEntityClass() );
      else
        return md.toObject( entityNext );
    }

    @Override
    public void remove()
    {
      d_iterSrc.remove();
    }

    @Override
    public Cursor getCursor()
    {
      return d_iterSrc.getCursor();
    }
  }

  /**
   * List that translates all retrieved entities into typed objects.
   */
  class ToObjectList<S> implements QueryResultList<S>
  {
    private final List<S> d_list;
    private final Cursor  d_cursorInternal;

    @SuppressWarnings( "unchecked" )
    public ToObjectList( QueryResultList<Entity> listSrc, boolean fKeysOnly )
    {
      d_list = new ArrayList<S>( listSrc.size() );
      d_cursorInternal = listSrc.getCursor();

      for ( Entity ent : listSrc )
      {
        TMetaData<S> md = (TMetaData<S>) d_factory.getMetaData( ent );

        if ( fKeysOnly )
          d_list.add( (S) d_factory.rawKeyToTKey( ent.getKey(), md.getEntityClass() ) );
        else
          d_list.add( md.toObject( ent ) );
      }
    }

    @Override
    public Cursor getCursor()
    {
      return d_cursorInternal;
    }

    @Override
    public boolean add( S obj )
    {
      return d_list.add( obj );
    }

    @Override
    public void add( int nIndex, S obj )
    {
      d_list.add( nIndex, obj );
    }

    @Override
    public boolean addAll( Collection<? extends S> coll )
    {
      return d_list.addAll( coll );
    }

    @Override
    public boolean addAll( int nIndex, Collection<? extends S> coll )
    {
      return d_list.addAll( nIndex, coll );
    }

    @Override
    public void clear()
    {
      d_list.clear();
    }

    @Override
    public boolean contains( Object obj )
    {
      return d_list.contains( obj );
    }

    @Override
    public boolean containsAll( Collection<?> coll )
    {
      return d_list.containsAll( coll );
    }

    @Override
    public S get( int nIndex )
    {
      return d_list.get( nIndex );
    }

    @Override
    public int indexOf( Object obj )
    {
      return d_list.indexOf( obj );
    }

    @Override
    public boolean isEmpty()
    {
      return d_list.isEmpty();
    }

    @Override
    public Iterator<S> iterator()
    {
      return d_list.iterator();
    }

    @Override
    public int lastIndexOf( Object obj )
    {
      return d_list.lastIndexOf( obj );
    }

    @Override
    public ListIterator<S> listIterator()
    {
      return d_list.listIterator();
    }

    @Override
    public ListIterator<S> listIterator( int nIndex )
    {
      return d_list.listIterator( nIndex );
    }

    @Override
    public boolean remove( Object obj )
    {
      return d_list.remove( obj );
    }

    @Override
    public S remove( int nIndex )
    {
      return d_list.remove( nIndex );
    }

    @Override
    public boolean removeAll( Collection<?> coll )
    {
      return d_list.removeAll( coll );
    }

    @Override
    public boolean retainAll( Collection<?> coll )
    {
      return d_list.retainAll( coll );
    }

    @Override
    public S set( int nIndex, S obj )
    {
      return d_list.set( nIndex, obj );
    }

    @Override
    public int size()
    {
      return d_list.size();
    }

    @Override
    public List<S> subList( int nFrom, int nTo )
    {
      return d_list.subList( nFrom, nTo );
    }

    @Override
    public Object[] toArray()
    {
      return d_list.toArray();
    }

    @Override
    public <K> K[] toArray( K[] aobj )
    {
      return d_list.toArray( aobj );
    }
  }
}
